package pl.agh.pp.zephyr.integration;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;

import pl.agh.pp.zephyr.algorithms.CallRouter;
import pl.agh.pp.zephyr.algorithms.RouteRequest;
import pl.agh.pp.zephyr.algorithms.RouteRequestFactory;
import pl.agh.pp.zephyr.logic.Engine;
import pl.agh.pp.zephyr.logic.SpringFactory;
import pl.agh.pp.zephyr.model.Gatekeeper;
import pl.agh.pp.zephyr.utils.LogPublisher;

/**
 * Kontroler gnu gatekeepera, pozwala wysyłać do niego komunikaty i reagować na
 * odebrane odpowiedzi.
 * */
public class GkController extends LogPublisher implements H323Controller {

	final CallRouter callRouter;

	// proste komendy dla gk
	public enum SimpleCommand {
		PRINT_ALL_REGISTRATIONS, PRINT_CURRENT_CALLS, RELOAD_CONFIGURATION, VERSION, STATISTICS
	}

	public static final Map<SimpleCommand, String> mapping;

	static {
		final Map<SimpleCommand, String> tmp = new HashMap<SimpleCommand, String>();
		tmp.put(SimpleCommand.PRINT_ALL_REGISTRATIONS, "PrintAllRegistrations");
		tmp.put(SimpleCommand.PRINT_CURRENT_CALLS, "PrintCurrentCalls");
		tmp.put(SimpleCommand.RELOAD_CONFIGURATION, "Reload");
		tmp.put(SimpleCommand.VERSION, "Version");
		tmp.put(SimpleCommand.STATISTICS, "Statistics");
		mapping = Collections.unmodifiableMap(tmp);
	}

	private final Engine engine;

	private Gatekeeper g;

	public GkController(GkClient client, Engine engine, CallRouter callRouter) {
		super(GkController.class);
		this.client = client;
		this.engine = engine;
		this.callRouter = callRouter;
	}

	private GkClient client;

	private Thread t;

	public void destroy() {
		if (client != null && client.isActive()) {
			log.info("stopping gatekeeper client...");
			client.removeHandler(handler);
			client.stop();
			try {
				t.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
				log.severe(e.getLocalizedMessage());
			} finally {
				SpringFactory.getBean(Engine.class).unregisterController(this);
				client = null;
				log.info("gatekeeper stopped...");
			}
		}
	}

	public void commandSimple(SimpleCommand com) {
		client.sendGkCommand(mapping.get(com));
	}

	/**
	 * Send DisconnectEndpoint command to GnuGk.
	 * 
	 * @param _epid
	 *            endpoint ID
	 */
	public void disconnectEndpoint(String _epid) {
		client.sendGkCommand("DisconnectEndpoint " + _epid);
	}

	/**
	 * Send DisconnectAlias command to GnuGk.
	 * 
	 * @param _alias
	 *            endpoint alias
	 */
	public void disconnectAlias(String _alias) {
		client.sendGkCommand("DisconnectAlias " + extractAliasName(_alias));
	}

	/**
	 * Send UnregisterAlias command to GnuGk.
	 * 
	 * @param _alias
	 *            endpoint alias
	 */
	public void unregisterAlias(String _alias) {
		client.sendGkCommand("UnregisterAlias " + extractAliasName(_alias));
	}

	/**
	 * Send MakeCall command to GnuGk.
	 * 
	 * @param _epid
	 *            source alias
	 * @param _destination
	 *            destination alias
	 */
	public void makeCall(String _source, String _destination) {
		client.sendGkCommand("MakeCall " + _source + " " + _destination);
	}

	public void transferCall(String _alias, String _destination) {
		log.info("TransferCall: " + extractAliasName(_alias) + " -> "
				+ _destination + "\n");
		client.sendGkCommand("TransferCall " + extractAliasName(_alias) + " "
				+ _destination);
	}

	/**
	 * send RouteToAlias response to GnuGk.
	 * 
	 * @param _alias
	 *            endpoint alias
	 * @param callingEpId
	 *            endpoint ID of calling endpoint
	 * @param callRef
	 *            call reference
	 */
	public void routeToAlias(String _alias, String callingEpId, String callRef) {
		StringBuilder sb = new StringBuilder("RouteToAlias ");
		sb.append(extractAliasName(_alias));
		sb.append(" ");
		sb.append(callingEpId);
		sb.append(" ");
		sb.append(callRef);
		String s = sb.toString();
		client.sendGkCommand(s);
	}

	/**
	 * send RouteToGateway response to GnuGk: RouteToGateway Alias IP:Port
	 * CallingEndpointID CallRef [CallID [CLI]]
	 * 
	 * @param _alias
	 *            endpoint alias
	 * @param ip
	 *            gatewayIp
	 * @param port
	 *            gateway port
	 * 
	 * @param callingEpId
	 *            endpoint ID of calling endpoint
	 * @param callRef
	 *            call reference
	 */
	public void routeToGateway(String _alias, String ip, String port,
			String callingEpId, String callRef) {
		StringBuilder sb = new StringBuilder("RouteToGateway ");
		sb.append(extractAliasName(_alias));
		sb.append(" ");
		sb.append(ip);
		if (port != null) {
			sb.append(":");
			sb.append(port);
		}
		sb.append(" ");
		sb.append(callingEpId);
		sb.append(" ");
		sb.append(callRef);
		client.sendGkCommand(sb.toString());
	}

	/**
	 * send RouteReject response to GnuGk.
	 * 
	 * @param callingEpId
	 *            endpoint ID of calling endpoint
	 * @param callRef
	 *            call reference
	 */
	public void routeReject(String callingEpId, String callRef) {
		client.sendGkCommand("RouteReject " + callingEpId + " " + callRef);
	}

	/**
	 * remove the type from the alias string
	 */
	private String extractAliasName(String _alias) {
		String aliasName = _alias;
		if (_alias.indexOf(":") > 0) {
			aliasName = _alias.substring(0, _alias.indexOf(":"));
		}
		return aliasName;
	}

	private final Handler handler = new Handler() {

		/**
		 * Bardzo ważna metoda - określa co się dzieje, gdy ze status porta
		 * napływają komunikaty
		 * 
		 * patrz rozdział 13.3 z dokumentacji GnuGk
		 * 
		 * TODO przeparsować wszystkie komunikaty, choćby na zwykły tekst
		 */
		@Override
		public void publish(LogRecord record) {
			if (Level.CONFIG.equals(record.getLevel())) {
				String s = record.getMessage();
				String data[] = s.split("#COMMAND#");
				String hostData[] = data[0].split(":");
				if (hostData.length == 2 && data.length == 2
						&& getHost().equals(hostData[0])
						&& getPort().equals(hostData[1])) 
				{
					s = data[1];
					String[] fs = splitFields(s);
					String d = null;
					if (s.startsWith("GCF")) {
						d = "GatekeeperConfirm";
					} else if (s.startsWith("GRJ")) {
						d = "GatekeeperReject";
					} else if (s.startsWith("RCF")) {
						d = "RegistrationConfirm";

						if (g == null) {
							for (Gatekeeper gk : engine.area.gks) {
								if (gk.ip.equals(getHost())
										&& gk.port.equals(getPort())) {
									g = gk;
									break;
								}
							}
						}
						if (g == null) {
							throw new RuntimeException(
									"Model inconsistent - gatekeeper of existing controller not found.");
						}
						// 4te pole w RCF to endpoint_type
						if ("mcu".equalsIgnoreCase(fs[3])) {
							// TODO automated mcu registration
							/*
							 * try { engine.registerMcu(fs[1].split(":")[0],
							 * Integer.valueOf(fs[1].split(":")[1])); } catch
							 * (NumberFormatException e) { // TODO
							 * Auto-generated catch block e.printStackTrace(); }
							 * catch (Exception e) { // TODO Auto-generated
							 * catch block e.printStackTrace(); }
							 */
						} else if ("terminal".equalsIgnoreCase(fs[3])) {
							// user registration
							engine.area.registerUser(
									fs[1].split(":")[0],
									fs[1].split(":").length > 1 ? fs[1]
											.split(":")[1] : "", fs[2], g
											.hashCode());
							// TODO exception handling
						} else {
							log.info("unknown endpoint type: " + fs[3]);
						}
					} else if (s.startsWith("RRJ")) {
						d = "RegistrationReject";
					} else if (s.startsWith("ACF")) {
						d = "AdmissionConfirm";
					} else if (s.startsWith("ARJ")) {
						d = "AdmissionReject";
					} else if (s.startsWith("DCF")) {
						d = "DisengageConfirm";
					} else if (s.startsWith("DRJ")) {
						d = "DisengageReject";
					} else if (s.startsWith("BCF")) {
						d = "BandwidthConfirm";
					} else if (s.startsWith("BRJ")) {
						d = "BandwidthReject";
					} else if (s.startsWith("LCF")) {
						d = "LocationConfirm";
					} else if (s.startsWith("LRJ")) {
						d = "LocationReject";
					} else if (s.startsWith("UCF")) {
						d = "UnregistrationConfirm";
					} else if (s.startsWith("URJ")) {
						d = "UnregistrationReject";
					} else if (s.startsWith("IRQ")) {
						d = "InfoRequest";
					} else if (s.startsWith("UCF")) {
						d = "UnregistrationRequest";
					} else if (s.startsWith("CDR")) {
						d = "Call detail record";
					} else if (s.startsWith("RouteRequest") && (fs.length >= 6)) {
						final String callerIP = fs[1].split(":")[0];
						final String callerPort = fs[1].split(":").length > 1 ? fs[1]
								.split(":")[1] : "";
						final String callerEndpointId = fs[2];
						final String callRef = fs[3];
						final String virtualQueue = fs[4];
						final String callerAlias = fs[5];
						final String callId = fs[6];
						final String calledIP = fs[7].split(":")[0];
						final String calledPort = fs[7].split(":").length > 1 ? fs[7]
								.split(":")[1] : "";

						if (callerAlias.contains("MCU")) {
							String[] called = virtualQueue.split("#");
							routeToGateway(called[0], called[1], called[2],
									callerEndpointId, callRef);
						} else {
							RouteRequest rr = null;
							try {
								rr = SpringFactory.getBean(
										RouteRequestFactory.class)
										.createRouteRequest(callerIP,
												callerPort, callerEndpointId,
												callRef, virtualQueue,
												callerAlias, callId, calledIP,
												calledPort);
								// TODO wysłać do routera
								log.info("got RouteRequest: " + rr);
								log.info("sending to router...");
								callRouter.routeCall(rr);

							} catch (Exception e) {
								log.severe("failed to build RouteRequest("
										+ callerAlias + " -> " + virtualQueue
										+ "): " + e.getMessage());
								// TODO poprawić formę logowania
								e.printStackTrace();
								routeReject(callerEndpointId, callRef);
								log.severe("request rejected");
							}
						}
					} else { // odpowiedź opisowa, np version
						d = "custom reply: " + s;
					}

					log.info(d + " "
							+ (fs.length > 0 ? Arrays.toString(fs) : ""));
				}
			}
		}

		@Override
		public void flush() {
		}

		@Override
		public void close() throws SecurityException {
		}
	};

	public void init(String host, int port, String username, String password)
			throws Exception {
		client.addHandler(handler);
		client.setPassword(password);
		client.setPort(port);
		client.setUser(username);
		client.setHost(host);
		client.init();
		t = new Thread(client);
		t.start();
		// TODO reszta inicjalizacji, może jakieś inne bajery
		this.commandSimple(SimpleCommand.PRINT_ALL_REGISTRATIONS);
	}

	private static String[] splitFields(String aString) {
		// remove whitespace
		aString = aString.trim();
		// remove semicolon
		if (aString.endsWith(";")) {
			aString = aString.substring(0, aString.lastIndexOf(";"));
		}
		return aString.split("\\|");
	}

	public String getHost() {
		return client.getHost();
	}

	public String getPort() {
		return client.getPort() + "";
	}

	@Override
	public boolean isActive() {
		return client.isActive();
	}

}
